import * as sagaEffects from 'redux-saga/lib/effects'
import {
    takeEveryHelper as takeEvery,
    takeLatestHelper as takeLatest,
    throttleHelper as throttle
} from 'redux-saga/lib/internal/sagaHelpers'
import prefixType from './prefixType'

export default function getSaga (effects, model, onError, onEffect) {
    return function * () {
        for (const key in effects) {
            if (Object.prototype.hasOwnProperty.call(effects, key)) {
                const watcher = getWatcher(key, effects[key], model, onError, onEffect)
                const task = yield sagaEffects.fork(watcher)
                yield sagaEffects.fork(function *() {
                    yield sagaEffects.take(`${model.namespace}/@@CANCEL_EFFECTS`)
                    yield sagaEffects.cancel(task)
                })
            }
        }
    }
}

function getWatcher (key, _effect, model, onError, onEffect) {
    let effect = _effect
    let type = 'takeEvery'
    let ms

    if (Array.isArray(_effect)) {
        effect = _effect[0]
        
        const opts = _effect[1]
        if (opts && opts.type) {
            type = opts.type
            if (type === 'throttle') {
                invariant(
                    opts.ms,
                    'app.state: opts.ms should be defined if type is throttle'
                )
                ms = opts.ms
            }
        }

        invariant(
            ['watcher', 'takeEvery', 'takeLatest', 'throttle'].indexOf(type) > -1,
            'app.state: effect type should be takeEvery, takeLatest, throttle or watcher'
        )
    }

    function noop () {}

    function * sagaWithCatch (...args) {
        const { __dva_resolve: resolve = noop, __dva_reject: reject = noop } =
            args.length > 0 ? args[0] : {}

        try {
            yield sagaEffects.put({type: `${key}/@@start`})
            const ret = yield effect(...args.concat(createEffects(model)))
            yield sagaEffects.put({type: `${key}/@@end`})
            resolve(ret)
        } catch (e) {
            onError(e)
            if (!e._dontReject) {
                reject(e)
            }
        }
    }

    const sagaWithOnEffect = applyOnEffect (onEffect, sagaWithCatch, model, key)

    switch (type) {
        case 'watcher':
            return sagaWithCatch
        case 'takeLatest':
            return function * () {
                yield takeLatest(key, sagaWithOnEffect)
            }
        case 'throttle':
            return function *() {
                yield throttle(ms, key, sagaWithOnEffect)
            }
        default:
            return function * () {
                yield takeEvery(key, sagaWithOnEffect)
            }
    }
}

function createEffects (model) {
    function assertAction (type, name) {
        invariant(type, 'dispatch: action should Object with type')
    }

    // 代理put和take方法
    function put (action) {
        const { type } = action
        assertAction(type, 'sagaEffects.put')
        return sagaEffects.put({...action, type: prefixType(type, model)})
    }
    function take (type) {
        if (typeof type === 'string') {
            assertAction(type, 'sagaEffects.take')
            return sagaEffects.take(prefixType(type, model))
        } else {
            return sagaEffects.take(type)
        }
    }
    return {...sagaEffects, put, take}
}

function applyOnEffect (fns, effect, model, key) {
    for (const fn of fns) {
        effect = fn(effect, sagaEffects, model, key)
    }
    return effect
}